Дізнайтеся про об'єкти-значення в JavaScript для надійного та тестованого коду. Навчіться реалізовувати імутативні структури та підвищувати цілісність даних.
Об'єкт-значення в JavaScript модулях: Імутабельне моделювання даних
У сучасній JavaScript-розробці забезпечення цілісності даних та простоти супроводу має першорядне значення. Одним із потужних методів для досягнення цього є використання об'єктів-значень (Value Objects) у модульних JavaScript-застосунках. Об'єкти-значення, особливо в поєднанні з імутабельністю, пропонують надійний підхід до моделювання даних, що призводить до чистішого, більш передбачуваного та легкого для тестування коду.
Що таке об'єкт-значення?
Об'єкт-значення — це невеликий, простий об'єкт, що представляє концептуальне значення. На відміну від сутностей (entities), які визначаються своєю ідентичністю, об'єкти-значення визначаються своїми атрибутами. Два об'єкти-значення вважаються рівними, якщо їхні атрибути рівні, незалежно від їхньої ідентичності в пам'яті. Поширені приклади об'єктів-значень включають:
- Валюта: Представляє грошову вартість (наприклад, USD 10, EUR 5).
- Діапазон дат: Представляє дату початку та кінця.
- Адреса електронної пошти: Представляє дійсну адресу електронної пошти.
- Поштовий індекс: Представляє дійсний поштовий індекс для конкретного регіону (наприклад, 90210 у США, SW1A 0AA у Великій Британії, 10115 у Німеччині, 〒100-0001 в Японії).
- Номер телефону: Представляє дійсний номер телефону.
- Координати: Представляє географічне положення (широта та довгота).
Ключовими характеристиками об'єкта-значення є:
- Імутабельність (незмінність): Після створення стан об'єкта-значення не можна змінити. Це усуває ризик ненавмисних побічних ефектів.
- Рівність на основі значення: Два об'єкти-значення рівні, якщо їхні значення рівні, а не якщо вони є одним і тим же об'єктом у пам'яті.
- Інкапсуляція: Внутрішнє представлення значення приховане, а доступ надається через методи. Це дозволяє проводити валідацію та забезпечує цілісність значення.
Навіщо використовувати об'єкти-значення?
Використання об'єктів-значень у ваших JavaScript-застосунках надає кілька значних переваг:
- Покращена цілісність даних: Об'єкти-значення можуть застосовувати обмеження та правила валідації під час створення, гарантуючи, що використовуються лише дійсні дані. Наприклад, об'єкт-значення `EmailAddress` може перевірити, чи є вхідний рядок справді дійсним форматом електронної пошти. Це зменшує ймовірність поширення помилок у вашій системі.
- Зменшення побічних ефектів: Імутабельність унеможливлює ненавмисні зміни стану об'єкта-значення, що призводить до більш передбачуваного та надійного коду.
- Спрощене тестування: Оскільки об'єкти-значення є імутабельними, а їхня рівність базується на значенні, юніт-тестування стає набагато простішим. Ви можете просто створювати об'єкти-значення з відомими значеннями та порівнювати їх з очікуваними результатами.
- Підвищена чіткість коду: Об'єкти-значення роблять ваш код більш виразним і легким для розуміння, явно представляючи концепції домену. Замість передачі сирих рядків або чисел, ви можете використовувати об'єкти-значення, такі як `Currency` або `PostalCode`, роблячи наміри вашого коду яснішими.
- Покращена модульність: Об'єкти-значення інкапсулюють специфічну логіку, пов'язану з певним значенням, сприяючи розділенню відповідальності та роблячи ваш код більш модульним.
- Краща співпраця: Використання стандартних об'єктів-значень сприяє спільному розумінню між командами. Наприклад, усі розуміють, що представляє об'єкт 'Currency'.
Реалізація об'єктів-значень у JavaScript модулях
Розглянемо, як реалізувати об'єкти-значення в JavaScript за допомогою модулів ES, зосереджуючись на імутабельності та належній інкапсуляції.
Приклад: Об'єкт-значення EmailAddress
Розглянемо простий об'єкт-значення `EmailAddress`. Ми використаємо регулярний вираз для перевірки формату електронної пошти.
```javascript // email-address.js const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; class EmailAddress { constructor(value) { if (!EmailAddress.isValid(value)) { throw new Error('Неправильний формат електронної адреси.'); } // Приватна властивість (через замикання) let _value = value; this.getValue = () => _value; // Геттер // Запобігаємо змінам ззовні класу Object.freeze(this); } getValue() { return this.value; } toString() { return this.getValue(); } static isValid(value) { return EMAIL_REGEX.test(value); } equals(other) { if (!(other instanceof EmailAddress)) { return false; } return this.getValue() === other.getValue(); } } export default EmailAddress; ```Пояснення:
- Експорт модуля: Клас `EmailAddress` експортується як модуль, що робить його придатним для повторного використання в різних частинах вашого застосунку.
- Валідація: Конструктор перевіряє вхідну адресу електронної пошти за допомогою регулярного виразу (`EMAIL_REGEX`). Якщо адреса недійсна, він викидає помилку. Це гарантує, що створюються лише дійсні об'єкти `EmailAddress`.
- Імутабельність: `Object.freeze(this)` запобігає будь-яким змінам об'єкта `EmailAddress` після його створення. Спроба змінити заморожений об'єкт призведе до помилки. Ми також використовуємо замикання, щоб приховати властивість `_value`, роблячи неможливим прямий доступ до неї ззовні класу.
- Метод `getValue()`: Метод `getValue()` надає контрольований доступ до базового значення адреси електронної пошти.
- Метод `toString()`: Метод `toString()` дозволяє легко перетворити об'єкт-значення на рядок.
- Статичний метод `isValid()`: Статичний метод `isValid()` дозволяє перевірити, чи є рядок дійсною адресою електронної пошти, не створюючи екземпляр класу.
- Метод `equals()`: Метод `equals()` порівнює два об'єкти `EmailAddress` на основі їхніх значень, гарантуючи, що рівність визначається вмістом, а не ідентичністю об'єктів.
Приклад використання
```javascript // main.js import EmailAddress from './email-address.js'; try { const email1 = new EmailAddress('test@example.com'); const email2 = new EmailAddress('test@example.com'); const email3 = new EmailAddress('invalid-email'); // Це викличе помилку console.log(email1.getValue()); // Вивід: test@example.com console.log(email1.toString()); // Вивід: test@example.com console.log(email1.equals(email2)); // Вивід: true // Спроба змінити email1 викличе помилку (потрібен строгий режим) // email1.value = 'new-email@example.com'; // Помилка: Cannot assign to read only property 'value' of object '#Продемонстровані переваги
Цей приклад демонструє основні принципи об'єктів-значень:
- Валідація: Конструктор `EmailAddress` забезпечує перевірку формату електронної пошти.
- Імутабельність: Виклик `Object.freeze()` запобігає модифікації.
- Рівність на основі значення: Метод `equals()` порівнює адреси електронної пошти на основі їхніх значень.
Додаткові аспекти
Typescript
Хоча попередній приклад використовує чистий JavaScript, TypeScript може значно покращити розробку та надійність об'єктів-значень. TypeScript дозволяє визначати типи для ваших об'єктів-значень, забезпечуючи перевірку типів на етапі компіляції та покращуючи супровід коду. Ось як можна реалізувати об'єкт-значення `EmailAddress` за допомогою TypeScript:
```typescript // email-address.ts const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; class EmailAddress { private readonly value: string; constructor(value: string) { if (!EmailAddress.isValid(value)) { throw new Error('Неправильний формат електронної адреси.'); } this.value = value; Object.freeze(this); } getValue(): string { return this.value; } toString(): string { return this.value; } static isValid(value: string): boolean { return EMAIL_REGEX.test(value); } equals(other: EmailAddress): boolean { return this.value === other.getValue(); } } export default EmailAddress; ```Ключові покращення з TypeScript:
- Типова безпека: Властивість `value` явно типізована як `string`, а конструктор гарантує, що передаються лише рядки.
- Властивості тільки для читання: Ключове слово `readonly` гарантує, що властивість `value` може бути присвоєна лише в конструкторі, що додатково посилює імутабельність.
- Покращене автодоповнення коду та виявлення помилок: TypeScript забезпечує краще автодоповнення коду та допомагає виявляти помилки, пов'язані з типами, під час розробки.
Техніки функціонального програмування
Ви також можете реалізувати об'єкти-значення, використовуючи принципи функціонального програмування. Цей підхід часто включає використання функцій для створення та маніпулювання імутабельними структурами даних.
```javascript // currency.js import { isNil, isNumber, isString } from 'lodash-es'; function Currency(amount, code) { if (!isNumber(amount)) { throw new Error('Сума повинна бути числом'); } if (!isString(code) || code.length !== 3) { throw new Error('Код валюти повинен бути 3-літерним рядком'); } const _amount = amount; const _code = code.toUpperCase(); return Object.freeze({ getAmount: () => _amount, getCode: () => _code, toString: () => `${_code} ${_amount}`, equals: (other) => { if (isNil(other) || typeof other.getAmount !== 'function' || typeof other.getCode !== 'function') { return false; } return other.getAmount() === _amount && other.getCode() === _code; } }); } export default Currency; // Приклад // const price = Currency(19.99, 'USD'); ```Пояснення:
- Фабрична функція: Функція `Currency` діє як фабрика, створюючи та повертаючи імутабельний об'єкт.
- Замикання: Змінні `_amount` та `_code` знаходяться в області видимості функції, що робить їх приватними та недоступними ззовні.
- Імутабельність: `Object.freeze()` гарантує, що повернутий об'єкт не можна змінити.
Серіалізація та десеріалізація
При роботі з об'єктами-значеннями, особливо в розподілених системах або при зберіганні даних, вам часто доведеться їх серіалізувати (перетворювати у рядковий формат, наприклад JSON) та десеріалізувати (перетворювати назад із рядкового формату в об'єкт-значення). При використанні JSON-серіалізації ви зазвичай отримуєте сирі значення, що представляють об'єкт-значення (рядкове представлення, числове представлення тощо).
При десеріалізації переконайтеся, що ви завжди створюєте екземпляр об'єкта-значення заново, використовуючи його конструктор, щоб забезпечити валідацію та імутабельність.
```javascript // Серіалізація const email = new EmailAddress('test@example.com'); const emailJSON = JSON.stringify(email.getValue()); // Серіалізуємо базове значення console.log(emailJSON); // Вивід: "test@example.com" // Десеріалізація const deserializedEmail = new EmailAddress(JSON.parse(emailJSON)); // Створюємо об'єкт-значення заново console.log(deserializedEmail.getValue()); // Вивід: test@example.com ```Приклади з реального життя
Об'єкти-значення можна застосовувати в різних сценаріях:
- Електронна комерція: Представлення цін на товари за допомогою об'єкта-значення `Currency`, що забезпечує послідовну обробку валют. Валідація артикулів товарів за допомогою об'єкта-значення `SKU`.
- Фінансові застосунки: Обробка грошових сум та номерів рахунків за допомогою об'єктів-значень `Money` та `AccountNumber`, що забезпечує виконання правил валідації та запобігає помилкам.
- Географічні застосунки: Представлення координат за допомогою об'єкта-значення `Coordinates`, що гарантує, що значення широти та довготи знаходяться в допустимих діапазонах. Представлення країн за допомогою об'єкта-значення `CountryCode` (наприклад, "US", "GB", "DE", "JP", "BR").
- Управління користувачами: Валідація адрес електронної пошти, номерів телефонів та поштових індексів за допомогою спеціалізованих об'єктів-значень.
- Логістика: Обробка адрес доставки за допомогою об'єкта-значення `Address`, що гарантує наявність та коректність усіх необхідних полів.
Переваги поза кодом
- Покращена співпраця: Об'єкти-значення визначають спільний словник у вашій команді та проєкті. Коли всі розуміють, що таке `PostalCode` або `PhoneNumber`, співпраця значно покращується.
- Простіший онбординг: Нові члени команди можуть швидко зрозуміти доменну модель, усвідомивши призначення та обмеження кожного об'єкта-значення.
- Зменшене когнітивне навантаження: Інкапсулюючи складну логіку та валідацію в об'єктах-значеннях, ви звільняєте розробників, щоб вони могли зосередитися на бізнес-логіці вищого рівня.
Найкращі практики для об'єктів-значень
- Робіть їх маленькими та сфокусованими: Об'єкт-значення повинен представляти єдину, чітко визначену концепцію.
- Забезпечуйте імутабельність: Запобігайте змінам стану об'єкта-значення після його створення.
- Реалізуйте рівність на основі значення: Гарантуйте, що два об'єкти-значення вважаються рівними, якщо їхні значення рівні.
- Надайте метод `toString()`: Це полегшує представлення об'єктів-значень у вигляді рядків для логування та налагодження.
- Пишіть вичерпні юніт-тести: Ретельно тестуйте валідацію, рівність та імутабельність ваших об'єктів-значень.
- Використовуйте значущі імена: Вибирайте імена, які чітко відображають концепцію, яку представляє об'єкт-значення (наприклад, `EmailAddress`, `Currency`, `PostalCode`).
Висновок
Об'єкти-значення пропонують потужний спосіб моделювання даних у JavaScript-застосунках. Застосовуючи імутабельність, валідацію та рівність на основі значень, ви можете створювати більш надійний, легкий у супроводі та тестований код. Незалежно від того, чи створюєте ви невеликий веб-застосунок, чи масштабну корпоративну систему, включення об'єктів-значень у вашу архітектуру може значно покращити якість та надійність вашого програмного забезпечення. Використовуючи модулі для організації та експорту цих об'єктів, ви створюєте компоненти, які можна легко використовувати повторно, що сприяє більш модульній та добре структурованій кодовій базі. Використання об'єктів-значень — це значний крок до створення чистіших, надійніших та легших для розуміння JavaScript-застосунків для глобальної аудиторії.